/*
1. transect class to represents a linear transect.
2. dataStore class to encapsulate the semantics of specific groundwater data models.
Core VVG library; must load before app.js
Depends on: lib_bore.js, OpenLayers.js

@author: Armin Schmidt, A.Schmidt@GeodataWIZ.com
@version: 1.4
@date: 10 May 2012
@history: V1.4 - 10/05/2012 - Distance filter moved to transect class.
@history: V1.0 - 09/04/2012 - Start
*/
var OpenLayers = OpenLayers;  // To fool JSLint despite missing module.
var debug = debug;

(function (project) {     
	project.lib = project.lib || {};
	var lib = project.lib;  // Shorthand.

    /* 
	==== transect ====
	A 'transect' is a line between two points expressed in a metric coordinate system. 
	The class has the following properties:
		lineString: a two-point linestring geometry running from west to east.
		mathAngle: the angle in radians between the transect and the positive x-axis (as used in calculations like tan() etc.)
		azimuth: the azimuth of the transect in degrees. Clockwise from north, hence 0deg is north, 90deg is east.
		length: the length of the transect between the two points.
	*/
	lib.transect = function (param1, param2) {
		// If one parameter is provided it is a linestring geometry, a feature with a linestring, or a layer.
		// If two parameters are provided they are two points.
		var that = {};  // The main data object to hold all public properties and methods.
		
		// Private properties.
		// var ...
		// Private methods.
		var pointAngle = function (p1, p2) {
			// Angle from p1 to p2; mathematical in radians, i.e. 0 is positive x-axis, positive is counter-clockwise.
			return Math.atan2(p2.y-p1.y, p2.x-p1.x);
		};
		var init_fromTwoPoints = function (point1, point2) {
			// 'Constructor' from two points.
			var swapPoints = function (p1, p2) {
				var p;
				p = p1;
				p1 = p2;
				p2 = p;
			};
			if (point1.x > point2.x) {
				swapPoints(point1, point2);
			}
			that.lineString = new OpenLayers.Geometry.LineString([point1, point2]);
			that.mathAngle = pointAngle(point1, point2);
			that.azimuth = 90.0-that.mathAngle*180.0/Math.PI;
			if (that.azimuth < 0) {
				that.azimuth += 360.0;
			}
			that.length = point1.distanceTo(point2);
		};
		var init_fromObject = function (obj) {
			// 'Constructor' from a single object.
			var init_fromGeometry = function (geometry) {
				if (geometry.components && geometry.components.length >= 2) {
					// Use first and last point.
					init_fromTwoPoints(
						geometry.components[0], 
						geometry.components[geometry.components.length-1]
					);
				}
			};
			var init_fromFeature = function (feature) {
				init_fromGeometry(feature.geometry);
			};
			var init_fromLayer = function (layer) {
				if (layer.features.length >= 1) {
					init_fromFeature(layer.features[0]);  // Take the first feature.
				}
			};

			switch (obj.CLASS_NAME) {
				case 'OpenLayers.Layer.Vector':
					init_fromLayer(obj);
					break;
				case 'OpenLayers.Feature.Vector':
					init_fromFeature(obj);
					break;
				case 'OpenLayers.Geometry.LineString':
					init_fromGeometry(obj);
					break;
			}
		};
		
		// Public properties.
		that.lineString = undefined;
		that.mathAngle = 0;
		that.azimuth = 0;
		that.length = 0;
		// Public methods.
		that.orthogonalDistances = function (point) {
			// Return a tuple of {linearDistance, offset} for a point to the side of the transect.
			var startPoint = that.lineString.components[0];
			var distToStart = point.distanceTo(startPoint);
			var angleToStart = pointAngle(startPoint, point);
			var angleToLine = angleToStart-that.mathAngle;
			var linearDistance = distToStart*Math.cos(angleToLine);
			var offset = distToStart*Math.sin(angleToLine);  // Positive is to the left of the line (looking from the start point), negative is to the right of the line.
			return {
				linearDistance: linearDistance,
				offset: offset
			};
		};
		that.distanceFilter = function (distance, serverParams) {
			// Return a distance filter (i.e. buffer) for the transect, 
			// either with DWITHIN, or by constructing a buffer polygon, or by using a BBOX.
			var metresPerDegree = 112500.0;  // Approximate number of metres per longitude degree in Victoria.
			var olFilter;
			var lineString;
			
			switch (serverParams.bestDistanceFilter) {
				case OpenLayers.Filter.Spatial.DWITHIN:
					olFilter = new OpenLayers.Filter.Spatial({
						type: OpenLayers.Filter.Spatial.DWITHIN,
						distanceUnits: 'm',
						distance: distance.value/metresPerDegree,
						//distance: 0.05,  // This seems to be in units of the underlying data, regardless of any applied transformation, or distanceUnits.
						//distance: 1000,
						projection: 'EPSG:900913',
						value: that.lineString
					});
					break;
				default:
					olFilter = {};
					break;
			}
			return olFilter;
		};
		
		if (param2) {
			init_fromTwoPoints(param1, param2);
		} else {
			init_fromObject(param1);
		}
		return that;
	};

	/*
	==== dataStore ====
	The 'dataStore' class encapsulates all the information known about data from a WFS source,
	which is composed of knowledge of the database schema, what it means and how it is
	interpreted, especially as a boreTransect.
	properties: -
	methods:
		layerAll(): return a new WMS layer to represent all data in the store.
		requestTransect(transect, distance): request data from the WFS along the transect.
		requestCompleted(): return whether the request for the transect data has completed.
		requestDuration(): return the duration in milliseconds until completion of the request.
		layerTransect(): return a new WFS layer from the previously requested transect.
		boreTransect(): return a new boreTransect from the previously requested transect.
	*/
	lib.dataStore = function (dataStoreDescriptor) {
		/*
		dataStoreDescriptor has the following properties:
			parametersWMS
				name
				url
				params
			parametersWFS
				name
				serverParams
					bestDistanceFilter
					units
				params  // includes .url
			boreDescriptor
			featureToBore: function (feature) {
				...
				return bore;
			}
		*/
		var that = {};  // The main data object to hold all public properties and methods.
		
		// Private properties.
		var transectRequestState = {
			response: {},  // Holds the transect features returned from the WFS request.
			transect: {},  // Transect used in the request.
			distance: {},  // Distance used in the request.
			startTimeMilliSec: 0,
			callbackFunction: {},
			callbackParams: {
				duration: 0,
				hasCompleted: false,
				hasErrors: false  // Error checking not yet implemented but parameter is used. @@
			}
		};  
		// Private methods.
		var init = function (dataStoreDescriptor) {
			// 'Constructor'. May be empty.
		};
		var transectRequestCompleted = function () {
			// Called when the transect request has completed. Calls the external callback function with parameters.
			
			var updateFeatures = function (features, transect, distance) {
				// Remove those features that are beyond the distance from the transect
				// and add linearDistance and offset to the attributes of the others.
				var initialLength = features.length;
				var i = 0, 
					feature,
					point, 
					orthogonalDistances;
				while (i < features.length) {
					feature = features[i];
					point = feature.geometry;  // Assumed to be a point!
					orthogonalDistances = transect.orthogonalDistances(point);
					if (
						orthogonalDistances.linearDistance >= 0 &&
						orthogonalDistances.linearDistance <= transect.length &&
						orthogonalDistances.offset <= distance.value
					) {
						feature.attributes.linearDistance = orthogonalDistances.linearDistance;
						feature.attributes.offset = orthogonalDistances.offset;
						i += 1;
					} else {
						// Remove this feature. Leave i unchanged as after the removel it will point to the next feature.
						features.splice(i, 1);
					}
				}
				if (debug) {alert('Removed outliers to reduce features from '+initialLength+' to '+features.length);}
			};
			
			transectRequestState.callbackParams.duration = +new Date()-transectRequestState.startTimeMilliSec;
			transectRequestState.callbackParams.hasCompleted = true;
			// Now check for errors in transectRequestState.response. Not yet implemented ... @@
			if (!transectRequestState.callbackParams.hasErrors) {
				updateFeatures(
					transectRequestState.response.features,
					transectRequestState.transect,
					transectRequestState.distance
				);
			}
			// Called even in case of erros so that a waiting function doesn't hang.
			transectRequestState.callbackFunction(transectRequestState.callbackParams);
		};
		
		that.layerAll = function () {
			// Return all data as a new WMS layer.
			if (dataStoreDescriptor.parametersWMS.url) {
				return new OpenLayers.Layer.WMS(
					dataStoreDescriptor.parametersWMS.name,
					dataStoreDescriptor.parametersWMS.url,
					dataStoreDescriptor.parametersWMS.params
				);
			} else {
				return new OpenLayers.Layer.Vector('empty');
			}
		};
		that.requestTransect = function (transect, distance, callback) {
			// Request data from the WFS along the transect.
			// transect = {linestring, mathAngle, azimuth, length}; see above
			// distance = {value, units = 'm', otherwise assumed 'degrees'}
			// callback = function (): function to be called when the request has completed.
			
			// This has been moved into class transect.
			var transectFilter = function (transect, distance) {
				// Define the buffer filter for the transect, 
				// either with DWITHIN, or by constructing a buffer polygon, or by using a BBOX.
				var bestAvailableFilter = OpenLayers.Filter.Spatial.DWITHIN;
				var metresPerDegree = 112500.0;  // Approximate number of metres per longitude degree in Victoria.
				var olFilter;
				var lineString;
				
				var projections = {
					map:  {srsName: 'EPSG:900913'},	// Map is in spherical mercator = Google.
					display: {srsName: 'EPSG:4326'}		// Display is in WGS84 lat/lon.
				};
				var projection;
				for (projection in projections) {
					projections[projection].olProjection = new OpenLayers.Projection(projections[projection].srsName);
				}
				
				lineString = transect.lineString.clone();
				//lineString.transform(projections.display.olProjection, projections.map.olProjection);
				
				switch (bestAvailableFilter) {
					case OpenLayers.Filter.Spatial.DWITHIN:
						olFilter = new OpenLayers.Filter.Spatial({
							type: OpenLayers.Filter.Spatial.DWITHIN,
							distanceUnits: 'm',
							distance: distance.value/metresPerDegree,
							//distance: 0.05,  // This seems to be in units of the underlying data, regardless of any applied transformation, or distanceUnits.
							//distance: 1000,
							projection: projections.map.srsName,
							//projection: projections.display.srsName,
							value: lineString
						});
						break;
					default:
						olFilter = {};
						break;
				}
				return olFilter;
			};

			//var filter = transectFilter(transect, distance);
			var filter = transect.distanceFilter(distance, dataStoreDescriptor.parametersWFS.serverParams);
			// The following is a good idea to allow using an externally specified filter, but that will not work the second time the function is run.
			//dataStoreDescriptor.parametersWFS.params.defaultFilter = dataStoreDescriptor.parametersWFS.params.defaultFilter || filter;
			dataStoreDescriptor.parametersWFS.params.defaultFilter = filter;
			var protocol = new OpenLayers.Protocol.WFS(dataStoreDescriptor.parametersWFS.params);
			
			transectRequestState = {
				transect: transect,
				distance: distance,
				startTimeMilliSec: +new Date(),
				callbackFunction: callback || function () {},
				callbackParams: {
					duration: 0,
					hasCompleted: false,
					hasErrors: false  // Error checking not yet implemented but parameter is used. @@
				},
				response: protocol.read({
					callback: transectRequestCompleted
				})  // Start the WFS query.
			};
		};
		that.requestHasCompleted = function () {
			// Return whether the request for the transect data has completed.
			return transectRequestState.callbackParams.hasCompleted;
		};
		that.requestDuration = function () {
			// Return the duration in milliseconds required for the completion of the request.
			return transectRequestState.callbackParams.duration;
		};
		that.fillLayerFromTransect = function (layer, params) {
			// Add the features from the previously requested transect to the existing vector layer.

			var simplifiedFeatures = function (features, attribute) {
				// Crude simplification to remove duplicates in next-index features with the same attribue.
				// Works well with bore_id as this is usually the sort criterium of the table.
				var i,
					feature,
					lastAttributeValue = '',
					sFeatures = [];
				for (i = 0; i < features.length; i += 1) {
					feature = features[i];
					if (feature.attributes[attribute] !== lastAttributeValue) {
						sFeatures.push(feature);
						lastAttributeValue = feature.attributes[attribute];
					}
				}
				if (debug) {alert('Reduced features from '+features.length+' to '+sFeatures.length);}
				return sFeatures;
			};
				
			if (
				transectRequestState.callbackParams.hasCompleted &&
				!transectRequestState.callbackParams.hasErrors
			) {
				if (params && params.simplifyByAttribute) {
					layer.addFeatures(simplifiedFeatures(
						transectRequestState.response.features, 
						params.simplifyByAttribute
					));
				} else {
					layer.addFeatures(transectRequestState.response.features);
				}
			}
		};
		that.getLayerFromTransect = function (params) {
			// Return a new vector layer filled with the features from the previously requested transect.
			var layer = new OpenLayers.Layer.Vector(dataStoreDescriptor.parametersWFS.name);
			that.fillLayerFromTransect(layer, params);
			return layer;
		};
		that.boreTransect = function () {
			// Return a new boreTransect from the previously requested transect.
			var i, 
				feature, 
				bore;
			var thisBoreTransect = lib.bore.boreTransect(dataStoreDescriptor.boreDescriptor);
			if (
				transectRequestState.callbackParams.hasCompleted &&
				!transectRequestState.callbackParams.hasErrors
			) {
				for (i = 0; i < transectRequestState.response.features.length; i += 1) {
					feature = transectRequestState.response.features[i];
					bore = dataStoreDescriptor.featureToBore(feature);
					thisBoreTransect.addBore(bore);
				}
			}
			return thisBoreTransect;
		};
		
		init(dataStoreDescriptor);
		return that;
	};
	
	// Instances of dataStore.
	var dataCategories = lib.bore.dataCategories;  // Shorthand.
	// Bores with acquifers and waterlevels.
	lib.dataStoreAqWl = lib.dataStore({
		parametersWMS: {
			name: 'DPI-Vic bores WMS',
			url: 'http://siss.versi.edu.au/geoserver/wms?service=WMS',
			params: {
				layers: 'mv_vvg_bores_aq_wl', 
				transparent: true,
				isBaseLayer: false,
				singleTile: true,
				opacity: 0.6,
				visibility: true
			}
		},
		parametersWFS: {
			// This WFS featureType (i.e. table) returns the following attributes:
			// bore_id, site_id, geom_loc, rlns, bdepth, case_height, 
			// aquifername, from_depth, to_depth, date, depth, reduced_level, natsurf_level, 
			name: 'DPI-Vic bores WFS',
			serverParams: {
				bestDistanceFilter: OpenLayers.Filter.Spatial.DWITHIN,  // GetCapabilities is not reliable, especially if a SISS implementation is used.
				units: 'degrees'  // Could, in theory, be obtained from the WFS with a GetCapabilities. Hardcoded here, like all other parameters.
			},
			params: {
				url: 'http://siss.versi.edu.au/geoserver/wfs?service=WFS',
				featureType: 'mv_vvg_bores_aq_wl',
				geometryName: 'geom_loc',
				extractAttributes : true,
				//maxFeatures: 1000,
				srsName: 'EPSG:900913',  // SRS of the map.
				version: '1.1.0'
			}
		},
		boreDescriptor: {
			label: {
				dataCategory: dataCategories.DESCRIPTOR,
				attemptInterpolate: false,
				defaultValue: 'Bore ?'
			},
			linearDistance: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			offset: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			elevation: {
				dataCategory: dataCategories.DESCRIPTOR,
				zeroAllowed: false,
				defaultValue: 100.0
			},
			boreDepth: {
				dataCategory: dataCategories.DESCRIPTOR
			},
			groundwaterLevel: {
				dataCategory: dataCategories.LEVEL,
				interfaceIndex: 0,
				style: 'blue' // Or anything else that allows to style the level.
			},
			groundwaterLevelDate: {
				dataCategory: dataCategories.DATE,
				relatesTo: 'groundwaterLevel',
				attemptInterpolate: false,
				zeroAllowed: false,
				defaultValue: '1970-01-01 00:00',
				dateFormat: 'yyyy-MM-dd H:mm'
				// If 'boreDescriptor[key].dateFormat' is provided, parsing is done with
				// date.js by Matt Kruse http://www.mattkruse.com/javascript/date/
				// Mismatches between input and format are not tolerated.
				// If no 'dateFormat' is given JavaScript's 'Date.parse()' is used.
				// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/parse
			},
			aquiferTopLevel: {
				dataCategory: dataCategories.LEVEL
			},
			aquiferBottomLevel: {
				dataCategory: dataCategories.LEVEL
			}
		},
		featureToBore: function (feature) {
			// Fields of table mv_vvg_bores_aq_wl:
			// bore_id, site_id, geom_loc, rlns, bdepth, case_height, 
			// aquifername, from_depth, to_depth, date, depth, reduced_level, natsurf_level.
			var bore_id = feature.attributes.bore_id;
			var rlns = +feature.attributes.rlns;
			var bdepth = +feature.attributes.bdepth;
			var case_height = +feature.attributes.case_height;
			var from_depth = +feature.attributes.from_depth;
			var to_depth = +feature.attributes.to_depth;
			var date = feature.attributes.date;
			var depth = +feature.attributes.depth;
			// Automatically added.
			var linearDistance = feature.attributes.linearDistance;
			var offset = feature.attributes.offset;
			
			var aqTop, aqBot;
			if (from_depth) {
				aqTop = rlns-from_depth;
			} else {
				aqTop = rlns;
			}
			if (to_depth) {
				aqBot = rlns-to_depth;
			} else {
				aqBot = rlns-bdepth;  // Assume depth of bore if no acquifer bottom defined.
			}

			var bore = {
				label: 'Bore '+bore_id,
				linearDistance: linearDistance,
				offset: offset,
				elevation: rlns,
				boreDepth: bdepth,
				groundwaterLevel: rlns-(depth-case_height),
				groundwaterLevelDate: date,
				aquiferTopLevel: aqTop,
				aquiferBottomLevel: aqBot
			};
			return bore;
		}
	});
}(
window.vvg = window.vvg || {}
));
